// ListParser.cs
//
//  Major parts of this code is based on code from edtftpnet see CREDITS,
//  Copyright (C) 2004 Enterprise Distributed Technologies Ltd

//  Modifications by Christian Eide, Copyright (C) 2008-2009 
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//

using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;

namespace Sarnata.Net.BareFtp.Protocol.Ftp
{
	public class ListParser
	{	
		public static List<RemoteFile> ParseList(List<string> list)
		{
			if(list == null || list.Count < 1)
				return new List<RemoteFile>();

			
			string format1a = "MMM'-'d'-'yyyy";			string format1b = "MMM'-'dd'-'yyyy";
			string[] format1 = {format1a,format1b};
			string format2a = "MMM'-'d'-'yyyy'-'HH':'mm";
			string format2b = "MMM'-'dd'-'yyyy'-'HH':'mm";	
        	string format2c = "MMM'-'d'-'yyyy'-'H':'mm";         
        	string format2d = "MMM'-'dd'-'yyyy'-'H':'mm";
			string winformat = "MM'-'dd'-'yy hh':'mmtt";
        	string mlsdformat = "yyyyMMddHHmmss";
			
	        string[] format2 = {format2a,format2b,format2c,format2d};
        
			System.Globalization.CultureInfo ParsingCulture = System.Globalization.CultureInfo.InvariantCulture;
			
			bool sysUnix = false;
			bool mlsd = false;
			
			string teststr = string.Empty;
			foreach(string test in list)
			{
				if(!string.IsNullOrEmpty(test.Trim()))
				{
					teststr = test;
					break;
				}
			}
			
			if(teststr.StartsWith("-") || teststr.StartsWith("d") || teststr.StartsWith("l") || teststr.StartsWith("total"))
            	sysUnix = true;
			
			if(teststr.StartsWith("type="))
				mlsd = true;
			
            // TODO: I bet we have more systems out there...
            
			List<RemoteFile> files = new List<RemoteFile>();
			
			if(mlsd)
			{
				foreach(string s in list)
				{
					RemoteFile file = new RemoteFile();
					
					foreach(string fact in s.Split(';'))
					{
						if(fact.IndexOf('=') > 0)
						{
							string[] keyval = fact.Split('=');
							if(keyval[0].ToLower() == "type")
							{
								if(keyval[1].ToLower() == "file")
									file.IsDir = false;
								else
									file.IsDir = true;
							}
							else if(keyval[0].ToLower() == "size")
							{
								file.Size = Convert.ToInt64(keyval[1]);
							}
							else if(keyval[0].ToLower() == "modify")
							{
								file.LastModified = DateTime.ParseExact(keyval[1].Substring(0,14), mlsdformat,
		                                                ParsingCulture.DateTimeFormat, DateTimeStyles.None);
							}
							else if(keyval[0].ToLower() == "unix.mode")
							{
								file.Permissions = Sarnata.Net.BareFtp.Common.Utils.PermissionParser.ModeToRWXT(keyval[1]);
							}
							else if(keyval[0].ToLower() == "unix.uid")
							{
								file.Owner = keyval[1];
							}
							else if(keyval[0].ToLower() == "unix.gid")
							{
								file.Group = keyval[1];
							}
						}
						else
							file.Filename = fact.Trim();
						
					}
					files.Add(file);
				}
			
			}
			else if(sysUnix)
			{
				foreach(string s in list)
					{
						if(s.StartsWith("total") || string.IsNullOrEmpty(s.Trim()))
					continue;
					
					List<string> fields = getFields(s);
					
						
					////////////////
					// field pos
					int index = 0;
									// first field is perms
					string permissions = fields[index++];
					char ch = permissions[0];
					bool isDir = false;
					bool isLink = false;
					if (ch == 'd')
						isDir = true;
					else if (ch == 'l')
						isLink = true;
					
					// some servers don't supply the link count					
					int linkCount = 0;
		            if (Char.IsDigit(fields[index][0])) // assume it is if a digit
		            {
		                try
						{
							linkCount = System.Int32.Parse(fields[index++]);
						}
						catch (FormatException)
						{
						}
		            }
		            else if (fields[index][0] == '-') // IPXOS Treck FTP server
		            {
		                index++;
		            }

						
					// owner and group
					string owner = fields[index++];
					string group = fields[index++];
            	
					// size
					long size = 0L;
					string sizeStr = fields[index];
            		// some listings don't have owner - make owner -> group & group -> size in
            		// this case, and use the sizeStr for the start of the date
            		if (!Char.IsDigit(sizeStr[0]) && Char.IsDigit(group[0])) 
            		{
                		sizeStr = group;  
                		group = owner;
                		owner = "";
            		}
            		else 
            		{
                		index++; 
            		}
            
					try
					{
						size = Int64.Parse(sizeStr);
					}
					catch (FormatException)
					{
						throw new FormatException("Failed to parse size: " + sizeStr);
					}                     
            
		            // next 3 fields are the date time
		            
		            // we expect the month first on Unix. 
		            // Connect:Enterprise UNIX has a weird extra field here - we test if the 
		            // next field starts with a digit and if so, we skip it
		            if (Char.IsDigit(fields[index][0]))
		                index++;

					int dateTimePos = index;
					DateTime lastModified;
					StringBuilder stamp = new StringBuilder(fields[index++]);
					stamp.Append('-').Append(fields[index++]).Append('-');
					
					string field = fields[index++];
					if (field.IndexOf((System.Char) ':') < 0)
					{
						stamp.Append(field); // year
		                try
		                {
		                    lastModified = DateTime.ParseExact(stamp.ToString(), format1,
		                                                ParsingCulture.DateTimeFormat, DateTimeStyles.None);
		                }
		                catch (FormatException)
		                {
		                    //log.Error("Failed to parse date string '" + stamp.ToString() + "'");
		                    throw;
		                }
					}
					else
					{
						// add the year ourselves as not present
		                int year = ParsingCulture.Calendar.GetYear(DateTime.Now);
						stamp.Append(year).Append('-').Append(field);
		                try
		                {

		                    lastModified = DateTime.ParseExact(stamp.ToString(), format2,
		                                                ParsingCulture.DateTimeFormat, DateTimeStyles.None);
		                }
		                catch (FormatException)
		                {
		                    //log.Error("Failed to parse date string '" + stamp.ToString() + "'");
		                    throw;
		                }
						
						// can't be in the future - must be the previous year
		                // add 2 days for time zones (thanks hgfischer)
						if (lastModified > DateTime.Now.AddDays(2))
						{
		                    lastModified = lastModified.AddYears(-1);
						}
					}					
			
					
					
					// name of file or dir. Extract symlink if possible
					string name = null;
					string linkedname = null;
			
					// we've got to find the starting point of the name. We
					// do this by finding the pos of all the date/time fields, then
					// the name - to ensure we don't get tricked up by a userid the
					// same as the filename,for example
					int pos = 0;
					bool ok = true;
					for (int i = dateTimePos; i < dateTimePos + 3; i++)
					{
						pos = s.IndexOf(fields[i], pos);
						if (pos < 0)
						{
							ok = false;
							break;
						}
		                else {
		                    pos += fields[i].Length;
		                }
					}
					if (ok)
					{
		                string remainder = s.Substring(pos).Trim();
						if (!isLink)
							name = remainder;
						else
						{
							// symlink, try to extract it
							pos = remainder.IndexOf("->");
							if (pos <= 0)
							{
								// couldn't find symlink, give up & just assign as name
								name = remainder;
							}
							else
							{
								int len = 2; // Length of "->"
								name = remainder.Substring(0, (pos) - (0)).Trim();
								if (pos + len < remainder.Length)
									linkedname = remainder.Substring(pos + len).Trim();
							}
						}
					}
					else
					{
						throw new FormatException("Failed to retrieve name: " + s);
					}
					
					RemoteFile file = new RemoteFile();
					file.Filename = name;
					file.Size = size;
					file.LastModified = lastModified; 
					file.Owner = owner; 
					file.Group = group;
					file.Permissions =  permissions;
					file.IsDir =  isDir;
					file.IsLink =  isLink;
					file.LinkCount = linkCount;
					file.Linkdest = linkedname;
					
					files.Add(file);
				
						////////////////
				}
			}
			else
			{
				// Probably windows system..
				foreach(string s in list)
				{
					List<string> fields = getFields(s);
					
					// first two fields are date time
					DateTime lastModified = DateTime.ParseExact(fields[0] + " " + fields[1], 
                                                winformat, ParsingCulture.DateTimeFormat);
			
					// dir flag
					bool isDir = false;
					long size = 0L;
					if (fields[2].ToUpper().Equals("<DIR>"))
						isDir = true;
					else
					{
						try
						{
							size = Int64.Parse(fields[2]);
						}
						catch (FormatException)
						{
							throw new FormatException("Failed to parse size: " + fields[2]);
						}
					}
			
					// we've got to find the starting point of the name. We
					// do this by finding the pos of all the date/time fields, then
					// the name - to ensure we don't get tricked up by a date or dir the
					// same as the filename, for example
					int pos = 0;
					bool ok = true;
					for (int i = 0; i < 3; i++)
					{
						pos = s.IndexOf(fields[i], pos);
						if (pos < 0)
						{
							ok = false;
							break;
						}
		                else {
		                    pos += fields[i].Length;
		                }
					}
					if (ok)
					{
					
		            string name = s.Substring(pos).Trim();
					RemoteFile file = new RemoteFile();
					file.Filename = name;
					file.Size = size;
					file.LastModified = lastModified; 
					file.Owner = String.Empty; 
					file.Group = String.Empty;
					file.Permissions =  String.Empty;
					file.IsDir =  isDir;
					file.IsLink =  false;
					file.Linkdest = String.Empty;
					
					files.Add(file);
					}
					else
					{
						throw new FormatException("Failed to retrieve name: " + s);
					}
				
				}
			
			}
			
			return files;
			
		}
		
		public static List<string> getFields(string str) {
            List<string> fields = new List<string>();
            StringBuilder field = new StringBuilder();
            
            for (int i = 0; i < str.Length; i++) {
                char ch = str[i];
                if (!Char.IsWhiteSpace(ch))
                    field.Append(ch);
                else {
                    if (field.Length > 0) {
                        fields.Add(field.ToString());
						field.Length = 0;

                    }
                }
            }
            // pick up last field
            if (field.Length > 0) {
                fields.Add(field.ToString());
            }
            return fields;
        }
	}
}
